/// @file World.cpp
/// @brief the main open gl context window, giving drawing functionlities.
/// also creates all other managers/objects required for the simulation

#include <iostream>
#include <QString>
#include "math.h"

#include "ngl/Matrix.h"
#include "ngl/Transformation.h"
#include "ngl/TransformStack.h"
#include "ngl/NGLInit.h"
#include "ngl/Obj.h"
#include "ngl/Material.h"

#include "World.h"
#include "Configuration.h"


World::World(
                   QWidget* _parent
                  ) :
                  QGLWidget(_parent)
{
    // set this widget to have the initial keyboard focus
    setFocusPolicy(Qt::StrongFocus);
    setFocus();

    //initialise simulation
    initialiseSimulation();
}

void World::initialiseSimulation()
{
    //initialise all the objects

    //initialise mouse rotation values
    m_rotate=false;
    m_spinXFace=0;
    m_spinYFace=0;

    //initialise mouse movement values
    m_move=false;
    m_moveX=0;
    m_moveY=0;

    //init framerate and mouse move sensitivity
    Configuration::initialiseWindow(m_frameRate, m_mouseMoveSensitivity);

    //2d top view -> disabled by default
    m_enable2DTopView = false;
    setup2DTopView();

    //create camera
    createCamera();

    //create light
    createLight();

    //initialise our shader manager to \0null
    m_shaderManager = 0;

    //create the flock
    m_flockManager = new FlockManager(m_enable2DTopView);

    //create the obstacles and disable them
    m_obstacleManager = new ObstacleManager(false);

    //create the wall with radius = 20, wall enabled, with hard limiting
    m_wall = new Wall(20.0, true, false, 0.15);

    //start main loop
    m_timer = startTimer(m_frameRate);

    //hide help by default
    m_drawHelp = false;

    //initial camera z value
    m_lastCamZ = m_camera.getEye().m_z;
}

void World::deleteSimulationObjects()
{
    //kill timer
    killTimer(m_timer);

    //delete all the current objects associated with the world
    delete m_shaderManager;
    delete m_obstacleManager;
    delete m_wall;
    delete m_flockManager;
}

World::~World()
{
    //clean up ngl
    std::cout << "\nNGL Cleanup" << std::endl;

    ngl::NGLInit* Init = ngl::NGLInit::instance();
    Init->NGLQuit();
    std::cout << "NGL shut down safely\n" << std::endl;

    //delete all current managers
    deleteSimulationObjects();
}

// This virtual function is called the first time draw() is executed
void World::initializeGL()
{
    //init glew
    ngl::NGLInit* Init = ngl::NGLInit::instance();
    Init->initGlew();

    // enable depth testing for drawing
    glEnable(GL_DEPTH_TEST);

    //initialise shaders
    m_shaderManager = new ShaderManager();
    m_shaderManager->updateViewProjection(&m_camera);

    //initialise wall drawing
    m_wall->loadGeometry();

    //initialise obstacle drawing
    m_obstacleManager->loadGeometry();

    //initialise flock/boid drawing
    m_flockManager->loadGeometry();
}

//This virtual function is called whenever the widget has been resized.
void World::resizeGL
                (
                    const int _w,
                    const int _h
                )
{
    //set the viewport
    glViewport(0, 0, _w, _h);

    //calculate the aspect ratio
    m_aspect = (float)_w / _h;

    //update camera aspect
    m_camera.setAspect(m_aspect);

    //update shaders ProjectionMatrix
    m_shaderManager->updateProjection(&m_camera);
}

void World::paintGL()
{
    // clear the screen and depth buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glClearColor(0.4, 0.4, 0.4, 1);

    //update tx stack with global mouse movements
    ngl::Transformation trans;
    trans.setRotation(m_spinXFace, m_spinYFace, 0);
    m_transformStack.setGlobal(trans);

    //display wall
    m_wall->draw(m_transformStack, m_shaderManager, "Colour");

    //display flock boids
    m_flockManager->draw(m_transformStack, m_shaderManager);

    //display flock's tend toward marker
    m_flockManager->drawTendTowardTarget(m_transformStack, m_shaderManager, "Light");

    //display obstacles
    m_obstacleManager->draw(m_transformStack, m_shaderManager, "Light");

    //display stats menu
    displayHelp();
}

void World::displayHelp()
{
    //display help menu
    if (m_drawHelp == true)
    {
        // first we need to fallback to the fixed functionality GL pipeline
        glUseProgram(0);

        //create font
        QFont font;
        font.setBold(false);
        font.setPointSize(14);
        glColor3f(1,1,0);

        //display debug and stats info
        QString text;

        text=QString("[boid count : %1]").arg(m_flockManager->getNumberOfBoids());
        renderText(10,18 * 1,text,font);

        text=QString("neighbouring : [distance : %1] [fov : %2] [fov angle : %3]")
             .arg(m_flockManager->getNeighbouringDistance())
             .arg(m_flockManager->getEnableFOVNeighbouring() ? "on" : "off")
             .arg(m_flockManager->getFOVNeighbouringAngle());
        renderText(10,18 * 2,text,font);

        text=QString("obstacle : [inside influence sphere count : %1] [collision count : %2]")
             .arg(m_obstacleManager->getCountOfInfluenceSphereHit())
             .arg(m_obstacleManager->getCountOfCollisions());
        renderText(10,18 * 3,text,font);

        int statStartLine = 5;
        text=QString("cohesion : [%1] [weight : %2]")
             .arg(m_flockManager->getEnableCohesion() ? "on" : "off")
             .arg(m_flockManager->getCohesionWeight());
        renderText(10,18 * statStartLine++,text,font);

        text=QString("separation : [%1] [weight : %2] [distance : %3]")
             .arg(m_flockManager->getEnableSeparation() ? "on" : "off")
             .arg(m_flockManager->getSeparationWeight())
             .arg(m_flockManager->getSeparationDistance());
        renderText(10,18 * statStartLine++,text,font);

        text=QString("alignment : [%1] [weight : %2]")
             .arg(m_flockManager->getEnableAlignment() ? "on" : "off")
             .arg(m_flockManager->getAlignmentWeight());
        renderText(10,18 * statStartLine++,text,font);

        text=QString("obstacle : [%1] [avoidance : %2] [weight : %3]")
             .arg(m_obstacleManager->getEnableObstacle() ? "on" : "off")
             .arg(m_flockManager->getEnableObstacleAvoidance() ? "on" : "off")
             .arg(m_flockManager->getObstacleAvoidanceWeight());
        renderText(10,18 * statStartLine++,text,font);

        text=QString("tend toward : [%1] [weight : %2] [position : (%3 %4 %5)]")
             .arg(m_flockManager->getEnableTendToward() ? "on" : "off")
             .arg(m_flockManager->getTendTowardWeight())
             .arg(m_flockManager->getTendToward().m_x)
             .arg(m_flockManager->getTendToward().m_y)
             .arg(m_flockManager->getTendToward().m_z);
        renderText(10,18 * statStartLine++,text,font);

        text=QString("external force : [%1] [weight : %2] [force : (%3 %4 %5)]")
             .arg(m_flockManager->getEnableExternalForce() ? "on" : "off")
             .arg(m_flockManager->getExternalForceWeight())
             .arg(m_flockManager->getExternalForce().m_x)
             .arg(m_flockManager->getExternalForce().m_y)
             .arg(m_flockManager->getExternalForce().m_z);
        renderText(10,18 * statStartLine++,text,font);

        text=QString("wall : [%1] [limiting : %2] [soft weight : %3]")
             .arg(m_wall->getEnableWall() ? "on" : "off")
             .arg(m_wall->getHardWall() ? "hard" : "soft")
             .arg(m_wall->getSoftLimitWeight());
        renderText(10,18 * statStartLine++,text,font);

        text=QString("neighbour tracing : [%1] [boid no : %2] [pitch : %3] [yaw : %4]")
             .arg(m_flockManager->getEnableBoidNeighbourScreening() ? "on" : "off")
             .arg(m_flockManager->getNeighbourUnderScreen())
             .arg(m_flockManager->getPitchOfCurrentBoid())
             .arg(m_flockManager->getYawOfCurrentBoid());
        renderText(10,18 * statStartLine++,text,font);

   }

}

void World::timerEvent(QTimerEvent* _event)
{
    //move flock if flock's auto move is enabled
    if (m_flockManager->getEnableAutoFlock())
    {
        //move flock
        m_flockManager->moveFlock(m_wall, m_obstacleManager);

        //refresh display
        updateGL();
    }
}

void World::keyPress(QKeyEvent* _event )
{
    switch (_event->key())
    {
        //start/stop flock movement
        case Qt::Key_Q : { m_flockManager->toggleEnableAutoFlock(); break; }

        //single step
        case Qt::Key_W : { m_flockManager->moveFlock(m_wall, m_obstacleManager); break; }

        //debug info
        case Qt::Key_D : { m_drawHelp ^=true; break; }

        default : break;
    }

    // re-draw GL
    updateGL();
}

void World::mouseMoveEvent (QMouseEvent* _event)
{
    //as the left mouse is still pressed and it moves, we get the x and y spin value
    if(m_rotate && _event->buttons() == Qt::LeftButton)
    {
        m_spinYFace = ( m_spinYFace + (_event->x() - m_origX) ) % 360 ;
        m_spinXFace = ( m_spinXFace + (_event->y() - m_origY) ) % 360 ;
        m_origX = _event->x();
        m_origY = _event->y();

        // re-draw GL
        updateGL();
    }

    //move camera in the (x,y) plane
    if(m_move && _event->buttons() == Qt::RightButton)
    {
        m_moveX = _event->x() - m_origMoveX;
        m_moveY = _event->y() - m_origMoveY;
        m_origMoveX = _event->x();
        m_origMoveY = _event->y();

        //move camera eye n look by (dx, dy)
        m_camera.move(m_moveX / m_mouseMoveSensitivity, m_moveY / m_mouseMoveSensitivity, 0);

        //update view projection matrices of camera
        m_shaderManager->updateViewProjection(&m_camera);

        // re-draw GL
        updateGL();
    }
}

void World::mousePressEvent (QMouseEvent* _event)
{
    // store the value where the mouse was clicked (x,y) and set the Rotate flag to true
    if(_event->button() == Qt::LeftButton)
    {
        m_origX = _event->x();
        m_origY = _event->y();
        m_rotate =true;
    }

    //keep track of origin of x,y movement
    if(_event->button() == Qt::RightButton)
    {
        m_origMoveX = _event->x();
        m_origMoveY = _event->y();
        m_move =true;
    }
}

void World::mouseReleaseEvent (QMouseEvent* _event)
{
    //when the mouse button is released, we set Rotate to false
    if (_event->button() == Qt::LeftButton) m_rotate=false;

    //disable camera movement when right button is released
    if (_event->button() == Qt::RightButton) m_move=false;

}

void World::createCamera()
{
    //create camera from config parameters
    m_camera = Configuration::initialiseCamera(m_aspect);
}

void World::createLight()
{
    //set a material
    ngl::Material m(ngl::PEWTER); m.use();

    //create our light from config and enable it
    m_light = Configuration::initialiseLight(); m_light.enable();
}

void World::updateCameraZoom(int depth)
{
    //calculate the relative change in Z value
    float newZ = (float)(-depth);
    float changeInZ = newZ - m_lastCamZ;

    //calculate the small change in N that corresponds to the change in Z
    float dn = changeInZ / m_camera.getN().m_z;

    //slide the camera along the N vector by the change in N
    m_camera.slide(0, 0, dn);

    //save the new Z value
    m_lastCamZ = m_camera.getEye().m_z;

    //update view projection matrices of camera
    if ((m_shaderManager != NULL) && (m_shaderManager != 0)) m_shaderManager->updateViewProjection(&m_camera);

    //refresh display
    updateGL();
}

void World::setup2DTopView()
{
    if (m_enable2DTopView)
    {
        //2d top view enabled -> rotate our view to give a top view
        m_spinXFace=-90;
        m_spinYFace=0;
    }
}

void World::refreshSimulation
                        (
                            const bool _enable2DTopView,
                            const int _initialNoOfBoids
                        )
{
    //toggle 2d top view
    m_enable2DTopView = _enable2DTopView;

    //setup camera
    setup2DTopView();

    //reinitialise flock
    m_flockManager->reInitialise(_enable2DTopView, _initialNoOfBoids);

    //redraw world
    updateGL();
}
